<?php

namespace App\Common\Lib\Hyperf\QueryExtend\Traits;

use App\Common as com;
use App\Common\Lib\Hyperf\QueryExtend\Traits\QueryCacheModule as nameQueryCacheModule;
use App\Common\Lib\Hyperf\QueryExtend;
use BadMethodCallException;
use DateTime;
use Hyperf\Utils\Collection;
use \Carbon\Carbon;

trait QueryCacheModule {
	/**
	 * 指定要缓存查询多长时间的秒数或日期时间。
	 * The number of seconds or the DateTime instance
	 * that specifies how long to cache the query.
	 *
	 * @var int|Carbon|null
	 */
	protected $cacheTime;

	/**
	 * 查询缓存的标记。如果仅为特定的标记刷新缓存，则非常有用。
	 * The tags for the query cache. Can be useful
	 * if flushing cache for specific tags only.
	 *
	 * @var null|array
	 */
	protected ?array $cacheTags = null;

	/**
	 * 将出现在所有查询上的查询缓存标记。
	 * The tags for the query cache that
	 * will be present on all queries.
	 *
	 * @var null|array
	 */
	protected ?array $cacheBaseTags = null;

	/**
	 * 要使用的缓存驱动程序。
	 * The cache driver to be used.
	 *
	 * @var string
	 */
	protected string $cacheDriver;

	/**
	 * 将在每个缓存键生成时加上前缀的缓存前缀字符串
	 * A cache prefix string that will be prefixed
	 * on each cache key generation.
	 *
	 * @var string
	 */
	protected string $cachePrefix = 'sqlCache';

	/**
	 * 指定在缓存查询时应该使用的键是纯的还是散列的。
	 * Specify if the key that should be used when caching the query
	 * need to be plain or be hashed.
	 *
	 * @var bool
	 */
	protected bool $cacheUsePlainKey = false;

	/**
	 * 设置是否应该避免缓存。
	 * Set if the caching should be avoided.
	 *
	 * @var bool|null [null]未设置
	 */
	protected ?bool $avoidCache = null;

	/**
	 * Get the cache from the current query.
	 *
	 * @param array $columns
	 * @param string|null $id
	 * @return Collection
	 */
	protected function getFromQueryCache( string $method = 'get', $columns = [ '*' ], $id = null ):Collection {
		if( is_null( $this->columns ) ){
			$this->columns = $columns;
		}

		$key = $this->getCacheKey( 'get' );
		$cache = $this->getCache();
		$callback = $this->getQueryCacheCallback( $method, $columns, $id );
		$time = $this->getCacheTime();
		if( $time > 0 ){
			return $cache->remember( $key, $time, $callback );
		}
		//
		return $cache->rememberForever( $key, $callback );
	}

	/**
	 * Get the query cache callback.
	 *
	 * @param string $method
	 * @param array $columns
	 * @param string|null $id
	 * @return \Closure
	 */
	public function getQueryCacheCallback( string $method = 'get', $columns = [ '*' ], $id = null ) {
		return function () use ( $method, $columns ) {
			$this->avoidCache = true;

			return $this->{$method}( $columns );
		};
	}

	/**
	 * Get a unique cache key for the complete query.
	 *
	 * @param string $method
	 * @param string|null $id
	 * @param string|null $appends
	 * @return string
	 */
	protected function getCacheKey( string $method = 'get', $id = null, $appends = null ):string {
		$key = $this->generateCacheKey( $method, $id, $appends );
		$prefix = $this->getCachePrefix();

		return "{$prefix}:{$key}";
	}

	/**
	 * Generate the unique cache key for the query.
	 *
	 * @param string $method
	 * @param string|null $id
	 * @param string|null $appends
	 * @return string
	 */
	protected function generateCacheKey( string $method = 'get', $id = null, $appends = null ):string {
		$key = $this->generatePlainCacheKey( $method, $id, $appends );

		if( $this->shouldUsePlainKey() ){
			return $key;
		}

		return hash( 'sha256', $key );
	}

	/**
	 * Generate the plain unique cache key for the query.
	 *
	 * @param string $method
	 * @param string|null $id
	 * @param string|null $appends
	 * @return string
	 */
	public function generatePlainCacheKey( string $method = 'get', $id = null, $appends = null ):string {
		$name = $this->connection->getName();

		// Count has no Sql, that's why it can't be used ->toSql()
		if( $method === 'count' ){
			return $name . $method . $id . serialize( $this->getBindings() ) . $appends;
		}

		return $name . $method . $id . $this->toSql() . serialize( $this->getBindings() ) . $appends;
	}

	/**
	 * Flush the cache that contains specific tags.
	 *
	 * @param array $tags
	 * @return bool
	 */
	protected function flushQueryCache( array $tags = [] ):bool {
		$cache = $this->getCacheDriver();

		if( ! method_exists( $cache, 'tags' ) ){
			return false;
		}

		if( ! $tags ){
			$tags = $this->getCacheBaseTags();
		}

		foreach( $tags as $tag ){
			self::flushQueryCacheWithTag( $tag );
		}

		return true;
	}

	/**
	 * Flush the cache for a specific tag.
	 *
	 * @param string $tag
	 * @return bool
	 */
	protected function flushQueryCacheWithTag( string $tag ):bool {
		$cache = $this->getCacheDriver();

		try{
			return $cache->tags( $tag )->flush();
		} catch( BadMethodCallException $e ){
			return $cache->flush();
		}
	}

	/**
	 * 指示应该缓存查询结果。
	 * Indicate that the query results should be cached.
	 * @param Carbon|int|bool|null $expire 有效时间，单位：秒。[null]永久 [int>0]必须大于0 [bool=false]禁用缓存
	 * @return $this
	 */
	public function cache( $expire = null ):self {
		if( $expire === false ){
			$this->avoidCache = true;
		}else if( $expire instanceof Carbon || $expire > 0 ){
			$this->cacheTime = $expire;
			$this->avoidCache = false;
		}else{
			throw com\Lib\Err\Exception::makeOfNew( '未知参数', com\Lib\Err\Code::$sysFunParamError );
		}
		//
		return $this;
	}

	/**
	 * 是否使用缓存
	 * @return bool|null [true]使用 [false]未使用 [null]未设置
	 */
	public function isUseCache( $expire ):?bool {
		return $this->avoidCache;
	}

	/**
	 * 获取缓存时间属性。
	 * Get the cache time attribute.
	 *
	 * @return int|Carbon|null
	 */
	public function getCacheTime() {
		return $this->cacheTime;
	}

	/**
	 * 获取缓存时间属性。
	 * Get the cache time attribute.
	 *
	 * @return int|null
	 */
	public function getCacheTimeToSeconds():?int {
		$time = $this->getCacheTime();
		if( $time instanceof Carbon ){
			return $time->diffInSeconds();
		}else if( $time > 0 ){
			return $time;
		}else if( $time === null ){
			return null;
		}else{
			throw com\Lib\Err\Exception::makeOfNew( '', com\Lib\Err\Code::$sysAssertError );
		}
	}

	/**
	 * 指示应该永远缓存查询结果。
	 * Indicate that the query results should be cached forever.
	 *
	 * @return $this
	 */
/*	public function cacheForever():self {
		return $this->cache( null );
	}*/

	/**
	 * 指示不应该缓存查询。
	 * Indicate that the query should not be cached.
	 *
	 * @return $this
	 */
/*	public function doNotCache():self {
		$this->avoidCache = true;
		return $this;
	}*/

	/**
	 * Set the cache prefix.
	 *
	 * @param string $prefix
	 * @return $this
	 */
	public function cachePrefix( string $prefix ):self {
		$this->cachePrefix = $prefix;

		return $this;
	}

	/**
	 * Attach tags to the cache.
	 *
	 * @param array $cacheTags
	 * @return $this
	 */
	protected function cacheTags( array $cacheTags = [] ):self {
		$this->cacheTags = $cacheTags;

		return $this;
	}

	/**
	 * Append tags to the cache.
	 *
	 * @param array $cacheTags
	 * @return $this
	 */
	protected function appendCacheTags( array $cacheTags = [] ):self {
		$this->cacheTags = array_unique( array_merge( $this->cacheTags ?? [], $cacheTags ) );

		return $this;
	}

	/**
	 * Use a specific cache driver.
	 *
	 * @param string $cacheDriver
	 * @return $this
	 */
	protected function cacheDriver( string $cacheDriver ):self {
		$this->cacheDriver = $cacheDriver;

		return $this;
	}

	/**
	 * Set the base cache tags; the tags
	 * that will be present on all cached queries.
	 *
	 * @param array $tags
	 * @return $this
	 */
	public function cacheBaseTags( array $tags = [] ):self {
		$this->cacheBaseTags = $tags;

		return $this;
	}

	/**
	 * Use a plain key instead of a hashed one in the cache driver.
	 *
	 * @return $this
	 */
	public function withPlainKey():self {
		$this->cacheUsePlainKey = true;

		return $this;
	}

	/**
	 * Get the cache driver.
	 *
	 * @return QueryExtend\Cache\Driver
	 */
	protected function getCacheDriver():QueryExtend\Cache\Driver {
		return new QueryExtend\Cache\Driver();
	}

	/**W
	 * Get the cache object with tags assigned, if applicable.
	 *
	 * @return QueryExtend\Cache\Driver
	 */
	protected function getCache() {
		$cache = $this->getCacheDriver();

		$tags = array_merge( $this->getCacheTags() ?: [], $this->getCacheBaseTags() ?: [] );

		try{
			return $tags ? $cache->tags( $tags ) : $cache;
		} catch( BadMethodCallException $e ){
			return $cache;
		}
	}

	/**
	 * 检查是否应该避免缓存操作。
	 * Check if the cache operation should be avoided.
	 *
	 * @return bool
	 */
	public function shouldAvoidCache():bool {
		return $this->avoidCache;
	}

	/**
	 * 检查缓存操作键是否应该使用普通查询键。
	 * Check if the cache operation key should use a plain
	 * query key.
	 *
	 * @return bool
	 */
	public function shouldUsePlainKey():bool {
		return $this->cacheUsePlainKey;
	}

	/**
	 * 获取缓存标签属性。
	 * Get the cache tags attribute.
	 *
	 * @return array|null
	 */
	protected function getCacheTags() {
		return $this->cacheTags;
	}

	/**
	 * 获取基本缓存标签属性。
	 * Get the base cache tags attribute.
	 *
	 * @return array|null
	 */
	protected function getCacheBaseTags() {
		return $this->cacheBaseTags;
	}

	/**
	 * 获取缓存前缀属性。
	 * Get the cache prefix attribute.
	 *
	 * @return string
	 */
	public function getCachePrefix():string {
		return $this->cachePrefix;
	}
}
